1 module hip.gui.widget; 2 3 class Widget 4 { 5 struct Bounds 6 { 7 int x, y, width, height; 8 } 9 struct Transform 10 { 11 int x, y; 12 float rotation = 0, scaleX = 0, scaleY = 0; 13 } 14 int width, height; 15 16 protected Widget parent; 17 protected Widget[] children; 18 protected Transform worldTransform; 19 protected Transform localTransform; 20 protected bool visible = true; 21 protected bool isDirty = true; 22 23 Bounds getWorldBounds() 24 { 25 Bounds b = Bounds(worldTransform.x, worldTransform.y, width, height); 26 Bounds unmod = b; 27 import hip.api; 28 foreach(ch; children) 29 { 30 import hip.math.utils:min, max; 31 Bounds chBounds = ch.getWorldBounds; 32 b.x = min(b.x, chBounds.x); 33 b.y = min(b.y, chBounds.y); 34 b.width = max(b.width, chBounds.width+chBounds.x - unmod.x); 35 b.height = max(b.height, chBounds.height+chBounds.y - unmod.y); 36 } 37 return b; 38 } 39 40 Widget findWidgetAt(float[2] pos){return findWidgetAt(cast(int)pos[0], cast(int)pos[1]);} 41 Widget findWidgetAt(int x, int y) 42 { 43 import hip.math.collision; 44 foreach_reverse(w; children) 45 { 46 Bounds wb = w.getWorldBounds(); 47 if(w.visible && isPointInRect(x, y, wb.x, wb.y, wb.width, wb.height)) 48 return w.findWidgetAt(x, y); 49 } 50 51 Bounds wb = getWorldBounds(); 52 return isPointInRect(x, y, wb.x, wb.y, wb.width, wb.height) ? this : null; 53 } 54 55 Bounds getLocalBounds(){return Bounds(localTransform.x,localTransform.y,width,height);} 56 57 void setPosition(int x, int y) 58 { 59 isDirty = true; 60 localTransform.x = x; 61 localTransform.y = y; 62 setChildrenDirty(); 63 } 64 65 private void setChildrenDirty() 66 { 67 foreach(ch; children) 68 { 69 ch.isDirty = true; 70 ch.setChildrenDirty(); 71 } 72 } 73 74 private Widget getDirtyRoot() 75 { 76 Widget curr = parent; 77 Widget last = curr; 78 while(curr && curr.isDirty) 79 { 80 last = curr; 81 curr = curr.parent; 82 } 83 return curr is null ? last : curr; 84 } 85 86 private void updateWorldTransform(in Transform* parentTransform) 87 { 88 if(parentTransform is null) 89 worldTransform = localTransform; 90 else 91 { 92 alias p = parentTransform; 93 worldTransform.x = p.x+localTransform.x; 94 worldTransform.y = p.y+localTransform.y; 95 worldTransform.rotation = p.rotation+localTransform.rotation; 96 worldTransform.scaleX = p.scaleX*localTransform.scaleX; 97 worldTransform.scaleY = p.scaleY*localTransform.scaleY; 98 } 99 isDirty = false; 100 foreach(ch; children) 101 ch.updateWorldTransform(&worldTransform); 102 } 103 private void recalculateWorld() 104 { 105 if(isDirty) 106 { 107 Widget root = getDirtyRoot(); 108 if(root) 109 root.updateWorldTransform(root.parent ? &root.parent.worldTransform : null); 110 else 111 updateWorldTransform(parent ? &parent.worldTransform : null); 112 } 113 } 114 115 void addChild(scope Widget[] widgets...) 116 { 117 foreach(w; widgets) addChild(w); 118 } 119 void addChild(Widget w) 120 { 121 children~= w; 122 w.isDirty = true; 123 w.parent = this; 124 w.setChildrenDirty(); 125 } 126 127 void setParent(Widget w) 128 { 129 w.addChild(this); 130 } 131 132 //Event Methods 133 void onFocusEnter() 134 { 135 isFocused = true; 136 } 137 void onFocusExit() 138 { 139 isFocused = false; 140 } 141 142 void onScroll(float[3] currentScroll, float[3] lastScroll) 143 { 144 setPosition( 145 cast(int)(localTransform.x + currentScroll[0] - lastScroll[0]), 146 cast(int)(localTransform.y + currentScroll[1] - lastScroll[1]) 147 ); 148 } 149 ///Executed the first time the mouse enters in the widget's boundaries 150 void onMouseEnter(){} 151 ///Executed when the mouse goes down inside the widget 152 void onMouseDown(){} 153 ///Executed when both a mousedown and mouseup is executed when mouse is over this widget 154 void onMouseClick(){} 155 ///If onMouseDown was executed, onMouseUp will be called even if the mouse is not inside the widget 156 void onMouseUp(){} 157 void onMouseMove(){} 158 private int dragOffsetX, dragOffsetY; 159 void onDragStart(int x, int y) 160 { 161 dragOffsetX = worldTransform.x - x; 162 dragOffsetY = worldTransform.y - y; 163 } 164 void onDragged(int x, int y) 165 { 166 import hip.api; 167 setPosition(x + dragOffsetX, y + dragOffsetY); 168 } 169 void onDragEnd(){} 170 ///Returns whether it accepted the receive 171 bool onDropReceived(Widget w){return false;} 172 void onMouseExit(){} 173 bool isDraggable; 174 bool isFocused; 175 //End Event Methods 176 177 178 void update() 179 { 180 foreach(ch; children) ch.update(); 181 } 182 183 protected void preRender(){recalculateWorld();} 184 final void render() 185 { 186 preRender(); 187 onRender(); 188 } 189 void onRender(){foreach(ch; children) if(ch.visible) ch.render();} 190 } 191 192 interface IWidgetRenderer 193 { 194 void render(int x, int y, int width, int height); 195 } 196 197 class DebugWidgetRenderer : IWidgetRenderer 198 { 199 import hip.api.graphics.color; 200 import hip.math.random; 201 HipColor color; 202 this() 203 { 204 color[] = Random.rangeub(0, 255); 205 } 206 this(HipColor color){this.color = color;} 207 208 void render(int x, int y, int width, int height) 209 { 210 import hip.api.graphics.g2d.renderer2d; 211 fillRoundRect(x,y,width,height, 4, color); 212 } 213 }